--[[## ft=lua ##################################################################
  REXCEPT - Restartable EXCEPTions in Lua.

  Lua's error primitives are exactly this: primitive.  They are not particularly
  conducive to chaining exceptions, for example, and they are also incapable of
  doing in-situ retries: entire, potentially time-consuming operations must be
  repeated for errors which may be trivially correctable.

  This system replaces pcall() and error() with try() and either raise() for
  restartable exceptions or throw() for non-restartable.  Handlers can be
  changed and rethrowing is a valid option for C++- and Java-style exception
  handling discipline.  In addition, a handler can clear up an error condition
  (out of memory, say, or non-existent file) and tell the block that threw to
  try again without having to unwind a potentially large stack and rewind it to
  do it over from the beginning.

  Helper functions

  API
  ===

  `try(block, handlers)` - execute block and on error execute a handler keyed by
                           the thrown/raised exception
  `raise(exception)` - user level restartable exception
  `throw(exception)` - user level terminal exception
  `exception(message)` - build optional "smart exceptions" to include data

  ############################################################################]]

local counter = 1
local string = string
local table = table
local table = require 'tablex'

local function is_exception(exc)
    return exc and exc.__type == 'exception' and exc.__name
end

--[[####################################
  try(block, handlers)
  Speculatively execute `block` and, upon error, call the appropriate handler
  if available.  If an `exception()` was `raise()`ed, the handler is called and
  if returning 'true' the coroutine is `resume()`ed.  If it was `throw()`n it is
  not resumed and the coroutine is destroyed.
  ####################################]]
local function try(block, handlers)
    handlers = handlers or {}
    assert(type(block) == 'function', 'block must be a function')
    assert(type(handlers) == 'table', 'handlers must be in a table')
    for k,v in pairs(handlers) do
        assert(type(v) == 'function', 'handlers must be blocks')
    end

    -- wrap the supplied block in a coroutine
    local block_wrapper = coroutine.create(block)
    local args = {}

    -- keep executing the coroutine until something ends it
    while true do

        -- collect everything yielded from the coroutine
        local results = { coroutine.resume(block_wrapper, table.unpack(args)) }

        -- legacy errors and thrown exceptions put false in the first space
        if not results[1] then

            -- this is an exception
            if is_exception(results[2]) then
                local exc = results[2]
                -- if there's a handler registered, call it
                if handlers[exc.__name] then
                    handlers[exc.__name](exc)
                end
            -- this is a legacy error, so if there's a legacy handler …
            elseif handlers['legacy'] then
                -- … drop the leading false and call the legacy handler
                table.remove(results, 1)
                handlers['legacy'](table.unpack(results))
            end

            -- in all cases we break the loop and return
            break

        -- raised exceptions are a coroutine yield, thus start with true
        -- if the second argument is an exception, process it
        elseif is_exception(results[2]) then
            local exc = results[2]

            -- remove the true and the exception from the yielded results
            table.remove(results, 1)
            table.remove(results, 2)

            -- call the handler for the type if present
            if handlers[exc.__name] then
                -- the return value of the handler becomes the args for the next
                -- round through the loop, thus passing them to the raise() call
                args = { handlers[exc.__name](exc) }
                -- if the handler returns true, we do another round otherwise we
                -- break free
                if args[1] then
                    table.remove(args, 1)
                else
                    break;
                end
            end
        else
            break
        end
    end

    -- clean up the coroutine
    -- coroutine.close only exists in Lua 5.4
    if coroutine.close then coroutine.close(block_wrapper) end
    block_wrapper = nil
end

local function validate_exception(exc)
    assert(is_exception(exc))
end

local function throw(exc)
    validate_exception(exc)
    error(exc)
end

local function raise(exc)
    validate_exception(exc)
    return table.unpack { coroutine.yield(exc) }
end

local function exception(name)
    assert(type(name) == 'string')
    return setmetatable({
        __name = name,
        __type = 'exception',
    }, {
        __tostring = function(self)
            local rv = name .. '{data={'
            if self.data then
                for k, v in pairs(self.data) do
                    rv = rv .. tostring(k) .. '='
                    if type(v) == 'string' then rv = rv .. "'" end
                    rv = rv .. tostring(v)
                    if type(v) == 'string' then rv = rv .. "'" end
                    rv = rv .. ','
                end
            end
            rv = rv .. '},'
            if self.extra then
                for _, v in ipairs(self.extra) do
                    if type(v) == 'string' then rv = rv .. "'" end
                    rv = rv .. tostring(v)
                    if type(v) == 'string' then rv = rv .. "'" end
                    rv = rv .. ','
                end
            end
            return rv
        end,
        __call = function(self, data, ...)
            self.data = data
            self.extra = {...}
            self.__stack = debug.traceback(nil, 2)
            return self
        end,
    })
end

--[[############################################################################
  Since the 'return true for retry, false for abort' rule is not immediately
  obvious, and since encoding special values for these cases will make code
  messy and interact poorly with pcall() and other such facilities, it's easier
  to make special functions that do the job directly.
  ############################################################################]]
local function retry(...)
    return true, ...
end

local function abort()
    return false
end

--[[############################################################################
  The module table.
  ############################################################################]]
local M = {
    exception = exception,
    try = try,
    throw = throw,
    raise = raise,
    retry = retry,
    abort = abort,
}

--[[############################################################################
  Return an indirect table to protect from casual inspection of contents.
  ############################################################################]]
return setmetatable({}, { __index = M })
